<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Permissions-Policy" content="serial=(self)">
    <title>BT04-A 串口蓝牙通信工具</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Microsoft Yahei", Arial, sans-serif;
        }
        body {
            max-width: 900px;
            margin: 0 auto;
            padding: 1rem;
            background: #f5f5f5;
        }
        .container {
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 15px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        .serial-control {
            padding: 1.2rem;
            background: #2196F3;
            color: white;
        }
        .serial-control h1 {
            font-size: 1.5rem;
            margin-bottom: 1rem;
            text-align: center;
        }
        .btn-group {
            display: flex;
            gap: 0.8rem;
            flex-wrap: wrap;
        }
        .btn {
            padding: 0.7rem 1.2rem;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 1rem;
            transition: all 0.2s;
            flex: 1;
            min-width: 120px;
        }
        .btn-primary {
            background: white;
            color: #2196F3;
        }
        .btn-primary:hover {
            background: #f0f7ff;
        }
        .btn-danger {
            background: #f44336;
            color: white;
        }
        .btn-danger:hover {
            background: #d32f2f;
        }
        .btn:disabled {
            background: #cccccc;
            color: #666;
            cursor: not-allowed;
        }
        #status {
            margin-top: 1rem;
            padding: 0.8rem;
            border-radius: 6px;
            background: rgba(255,255,255,0.2);
            font-size: 0.9rem;
        }
        .error {
            background: rgba(244,67,54,0.2) !important;
            color: #ffebee !important;
        }
        .success {
            background: rgba(76,175,80,0.2) !important;
            color: #e8f5e9 !important;
        }
        .chat-section {
            padding: 1rem;
        }
        .chat-window {
            height: 400px;
            border: 1px solid #eee;
            border-radius: 8px;
            padding: 1rem;
            overflow-y: auto;
            margin-bottom: 1rem;
            background: #f9f9f9;
        }
        .message-item {
            margin-bottom: 1rem;
            max-width: 80%;
        }
        .message-self {
            margin-left: auto;
        }
        .message-device {
            margin-right: auto;
        }
        .message-bubble {
            padding: 0.8rem 1rem;
            border-radius: 18px;
            position: relative;
        }
        .message-self .message-bubble {
            background: #2196F3;
            color: white;
            border-bottom-right-radius: 4px;
        }
        .message-device .message-bubble {
            background: white;
            color: #333;
            border: 1px solid #eee;
            border-bottom-left-radius: 4px;
        }
        .message-time {
            font-size: 0.7rem;
            color: #999;
            margin-top: 0.3rem;
            text-align: right;
        }
        .message-device .message-time {
            text-align: left;
        }
        .input-area {
            display: flex;
            gap: 0.8rem;
            align-items: flex-end;
        }
        #messageInput {
            flex: 1;
            padding: 0.9rem 1rem;
            border: 1px solid #eee;
            border-radius: 8px;
            font-size: 1rem;
            resize: none;
            height: 60px;
            outline: none;
        }
        #messageInput:focus {
            border-color: #2196F3;
        }
        #sendBtn {
            padding: 0.9rem 1.5rem;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 1rem;
            transition: background 0.2s;
        }
        #sendBtn:hover {
            background: #1976D2;
        }
        #sendBtn:disabled {
            background: #cccccc;
            cursor: not-allowed;
        }
        @media (max-width: 768px) {
            .chat-window {
                height: 300px;
            }
            .btn-group {
                flex-direction: column;
            }
            .input-area {
                flex-direction: column;
                align-items: stretch;
            }
            #messageInput {
                height: 80px;
            }
        }
    </style>
</head>
<body>
<div class="container">
    <div class="serial-control">
        <h1>BT04-A 串口蓝牙通信工具</h1>
        <div class="btn-group">
            <button id="connectSerialBtn" class="btn btn-primary">连接串口（BT04-A）</button>
            <button id="disconnectBtn" class="btn btn-danger" disabled>断开连接</button>
        </div>
        <div id="status">就绪：请先配对 BT04-A，再点击「连接串口」</div>
    </div>

    <div class="chat-section">
        <div class="chat-window" id="chatWindow"></div>
        <div class="input-area">
            <textarea id="messageInput" placeholder="请输入要发送的文字..."></textarea>
            <button id="sendBtn" class="btn-primary" disabled>发送</button>
        </div>
    </div>
</div>

<script>
    // 核心变量
    let port = null;
    let reader = null;
    let isConnected = false;
    const encoder = new TextEncoder();
    const decoder = new TextDecoder('utf-8');

    // DOM 元素
    const connectSerialBtn = document.getElementById('connectSerialBtn');
    const disconnectBtn = document.getElementById('disconnectBtn');
    const status = document.getElementById('status');
    const chatWindow = document.getElementById('chatWindow');
    const messageInput = document.getElementById('messageInput');
    const sendBtn = document.getElementById('sendBtn');

    // 检查 Web Serial 支持性
    if (!('serial' in navigator)) {
        updateStatus('错误：当前浏览器不支持 Web Serial API（需 Chrome 89+）', 'error');
        connectSerialBtn.disabled = true;
    }

    // 连接串口
    connectSerialBtn.addEventListener('click', async () => {
        if (isConnected) return;

        try {
            updateStatus('正在选择串口设备...', '');
            port = await navigator.serial.requestPort();
            await port.open({
                baudRate: 9600,
                dataBits: 8,
                stopBits: 1,
                parity: 'none',
                flowControl: 'none'
            });

            isConnected = true;
            updateStatus(`已连接串口：${port.getInfo().usbProductName || 'BT04-A'}`, 'success');
            connectSerialBtn.disabled = true;
            disconnectBtn.disabled = false;
            sendBtn.disabled = false;

            // 后台读取数据（不阻塞主线程）
            readSerialData();

        } catch (error) {
            isConnected = false;
            port = null;
            updateStatus(`串口连接失败：${error.message}`, 'error');
        }
    });

    // 断开串口（核心：暴力重置状态，放弃原生 close 操作）
    disconnectBtn.addEventListener('click', () => {
        // 立即标记为断开，终止所有逻辑（非阻塞）
        isConnected = false;

        // 禁用按钮，更新状态（立即响应，不等待）
        disconnectBtn.disabled = true;
        updateStatus('正在断开串口连接...', '');

        // 异步执行清理操作（不阻塞主线程）
        setTimeout(async () => {
            try {
                // 1. 取消读取器（仅尝试，失败不影响）
                if (reader) {
                    await reader.cancel().catch(() => {});
                    reader.releaseLock().catch(() => {});
                    reader = null;
                }

                // 2. 放弃串口资源（不调用 close，避免卡死）
                port = null;

                // 3. 重置 UI 状态
                updateStatus('已成功断开串口连接', 'success');
                connectSerialBtn.disabled = false;
                sendBtn.disabled = true;

            } catch (error) {
                // 即使清理失败，也强制重置 UI
                updateStatus('已断开（部分资源未释放，可重新连接）', 'success');
                connectSerialBtn.disabled = false;
                sendBtn.disabled = true;
            }
        }, 100); // 延迟 100ms，确保主线程先响应
    });

    // 读取串口数据（后台非阻塞）
    async function readSerialData() {
        // 用 requestIdleCallback 确保不阻塞主线程
        const readLoop = async () => {
            if (!isConnected || !port || !port.readable) return;

            try {
                reader = port.readable.getReader();
                const { value, done } = await reader.read();
                reader.releaseLock();
                reader = null;

                if (done || !isConnected) return;

                const message = decoder.decode(value).trim();
                if (message) {
                    addMessageToChat(message, 'device');
                    updateStatus(`收到消息：${message}`, '');
                }

            } catch (error) {
                if (isConnected) {
                    updateStatus(`读取数据失败：${error.message}`, 'error');
                }
            }

            // 空闲时继续读取（非阻塞）
            if (isConnected) {
                requestIdleCallback(readLoop);
            }
        };

        requestIdleCallback(readLoop);
    }

    // 发送文字（简化，避免锁占用）
    sendBtn.addEventListener('click', async () => {
        const message = messageInput.value.trim();
        if (!message || !isConnected || !port || !port.writable) {
            updateStatus('无法发送：输入为空或串口未连接', 'error');
            return;
        }

        let writer = null;
        try {
            writer = port.writable.getWriter();
            const data = encoder.encode(message + '\n');
            await writer.write(data);

            addMessageToChat(message, 'self');
            messageInput.value = '';
            updateStatus(`已发送：${message}`, 'success');

        } catch (error) {
            updateStatus(`发送失败：${error.message}`, 'error');
        } finally {
            // 无论成败，立即释放写入器
            if (writer) {
                writer.releaseLock().catch(() => {});
            }
        }
    });

    // 回车键发送
    messageInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter' && !e.shiftKey) {
            e.preventDefault();
            sendBtn.click();
        }
    });

    // 添加消息到聊天窗口
    function addMessageToChat(message, type) {
        const messageItem = document.createElement('div');
        messageItem.className = `message-item message-${type}`;

        const now = new Date();
        const timeStr = now.toLocaleTimeString('zh-CN', {
            hour: '2-digit',
            minute: '2-digit',
            second: '2-digit'
        });

        messageItem.innerHTML = `
                <div class="message-bubble">${escapeHtml(message)}</div>
                <div class="message-time">${timeStr}</div>
            `;

        chatWindow.appendChild(messageItem);
        chatWindow.scrollTop = chatWindow.scrollHeight;
    }

    // 防XSS
    function escapeHtml(text) {
        return text
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    // 更新状态
    function updateStatus(message, type = '') {
        status.textContent = message;
        status.className = type;
        console.log('[串口状态]', message);
    }

    // 页面关闭时仅重置状态（不执行 close）
    window.addEventListener('beforeunload', () => {
        isConnected = false;
        port = null;
        reader = null;
    });
</script>
</body>
</html>